package com.sticktogether.navigation;


import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.GregorianCalendar;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.location.Location;
import android.util.Log;

import com.sticktogether.R;
import com.sticktogether.server.ServerCommunication;


/**
 * A Navigation object contains all information about the server response of a Google Directions request for public transport
 * The Transit mode for Google Directions is not yet available for all cities.
 *
 * Note:
 * 1.
 * 	If there are double appearing navigation steps -> they come from Google's server -> no bug in this code 
 * (possible fix: compare every step to the previous step -> if they equal -> delete one of them)
 * 2.
 *  Some code parts are commented because they are not needed and thus save computing time. 
 *  They are not deleted, in case the information will be needed later.
 * 
 * @author 	David Schattel
 * @since 	01.12.2012
 */
public class Navigation {

	
	
	/**
	 * URL information
	 */
	private final String DIRECTIONS_REQUEST_PREFIX_FOR_XML = "http://maps.googleapis.com/maps/api/directions/xml";
	private HttpURLConnection conn;
	private URL url;
	private String origin;
	private String destination;
	private final String sensor = "true";
	private long timeInSec;
	private boolean departure_time;
	private String mode;	
	private String language;
	
	/**
	 * Parameters containing the response information
	 */
	private NodeList resultNodeList = null;
	private XPath xpath = XPathFactory.newInstance().newXPath();
	private Document directionsResponseDocument = null;

	/**
	 * Parameter containing the results after analyzing the response
	 */
	private String responseStatus;
	private String prefix = "//leg[1]/step[";
	private ArrayList<Step> steps = new ArrayList<Step>();
	private String error = "";
	private ArrayList<Drawable> icons = new ArrayList<Drawable>();
	
	//private int duration_route_i;
	private String duration_route_S;
	//private int distance_route_i;
	//private String distance_route_S;
	//private Location start_route;
	//private Location end_route;
	private String start_address;
	private String end_address;
	//private long dep_time_s;
	private String dep_time_S;
	//private String dep_timeZone;
	//private long arr_time_s;
	private String arr_time_S;
	//private String arr_timeZone;
	
	
	
	/**
	 * Navigation Constructor
	 * 
	 * @param origin			Where to start?
	 * @param destination		Where to go?
	 * @param year				
	 * @param month				
	 * @param day				
	 * @param hours				
	 * @param minutes			
	 * @param mode				What way for traveling? transit, driving, walking,...
	 * @param departure_time	Is the given date departure time or not?
	 * @param pm				Is the given time in the afternoon?
	 */
	public Navigation(String origin, String destination, int year, int month, int day, int hours, int minutes, String mode, String language, boolean departure_time, boolean pm){
		this(origin, destination, calculateSeconds(year, month, day, hours, minutes, pm), mode, language, departure_time);
	
	}
	
	/**
	 * Constructor with time in milliseconds
	 * @param origin
	 * @param destination
	 * @param time
	 * @param mode
	 * @param language
	 * @param departure_time
	 */
	public Navigation(String origin, String destination, long time, String mode, String language, boolean departure_time){
		this.origin = origin;
		this.destination = destination;
		this.departure_time = departure_time;
		this.mode = mode;
		this.language = language;
		this.timeInSec = time;
		
		// request info and analyze response
		if(requestRouteInfo())
			if(checkResponseStatus()){
				getRouteInfo();
				fillSteps();
			}
	}
	
	/**
	 * Get seconds from a specific date
	 * @param year
	 * @param month
	 * @param day
	 * @param hours
	 * @param minutes
	 * @param pm		true -> time is given in pm, false -> time is given in am
	 * @return 			Seconds
	 */
	private static long calculateSeconds(int year, int month, int day, int hours, int minutes, boolean pm){
		//deal with am/pm
		if(pm && hours <= 12){
			if(hours != 0)
				hours += 12;
			if(hours == 24)
				hours = 0;
		}
		return (new GregorianCalendar(year, month-1, day, hours, minutes).getTimeInMillis()/1000);
	}
	
	private void getRouteInfo(){
		final String routePrefix = "//leg[1]/";
		
		//this.duration_route_i = Integer.parseInt(getString(routePrefix + "duration/value", "i"));
		this.duration_route_S = getString(routePrefix + "duration/text", "s");
		
		//this.distance_route_i = Integer.parseInt(getString(routePrefix + "distance/value", "i"));
		//this.distance_route_S = getString(routePrefix + "distance/text", "s");
		
		//this.start_route = getLocation(routePrefix + "start_location");
		//this.end_route = getLocation(routePrefix + "end_location");
		
		this.start_address = getString(routePrefix + "start_address", "s");
		this.end_address = getString(routePrefix + "end_address", "s");
		
		//this.dep_time_s = Long.parseLong(getString(routePrefix + "departure_time/value", "l"));
		this.dep_time_S = getString(routePrefix + "departure_time/text", "s");
		//this.dep_timeZone = getString(routePrefix + "departure_time/time_zone", "s");
		
		//this.arr_time_s = Long.parseLong(getString(routePrefix + "arrival_time/value", "l"));
		this.arr_time_S = getString(routePrefix + "arrival_time/text", "s");
		//this.arr_timeZone = getString(routePrefix + "arrival_time/time_zone", "s");
	}
	
	/**
	 * Fill the list of steps which describe the route
	 * 
	 */
	private void fillSteps() {
		// loop over all steps
    	for(int step = 1; true; step++){
    		
    		// get travel mode and break if there is no more travel mode/no steps
			resultNodeList = getNodeList(prefix + step + "]/" + "travel_mode");
	    	if(resultNodeList.getLength() == 0){
	    		break;
	    	}
		    
	    	if(!resultNodeList.item(0).getTextContent().equals("WALKING") && !resultNodeList.item(0).getTextContent().equals("TRANSIT")){
	    		this.error += ServerCommunication.map.getString(R.string.only_transit_available) + "\n" + getLegalInformation();
	    		break;
	    	}
	    	
	    	// initial id = 0, initial depth = 0 (see Step.java)
		    steps.add(new Step(directionsResponseDocument, xpath, prefix, step, 0, 0));
			    
    	}
	}
	
	/**
	 * Get LegalInformation
	 */
	public String getLegalInformation(){
		String legal = "";
		final String copyrights = getString("/DirectionsResponse/route/copyrights", "s");
		legal += "Copyrights: " + copyrights;
		
		final String warning = getString("/DirectionsResponse/route/warning", "s");
		legal += "\n"+ ServerCommunication.map.getString(R.string.warning) +": " + warning;
		
		return legal;
	}

	/**
	 * Get the Polyline overview
	 * @return points
	 * 
	 */
	public String getOverviewPolyline(){
    	
		return getString("/DirectionsResponse/route/overview_polyline/points", "s");
	}
	
	/**
	 * Get Map Bounds for included Waypoint coordinates for South West
	 * @return Location including latitude and longitude
	 * 
	 */
	public Location getBoundSW() {
		return getLocation("/DirectionsResponse/route/bounds/southwest");
	}
	
	/**
	 * Get Map Bounds for included Waypoint coordinates for North East
	 * @return Location including latitude and longitude
	 * 
	 */
	public Location getBoundNE(){
		return getLocation("/DirectionsResponse/route/bounds/northeast");
	}
	
	/**
	 * Get Response status from xml document
	 * @return false if no route has been returned
	 * 
	 */
	private boolean checkResponseStatus() {
		// get response status
    	responseStatus = getString("/DirectionsResponse/status", "s");
    	if(!responseStatus.equals("OK")){
    		this.error += "\n" + ServerCommunication.map.getString(R.string.only_transit_available);
    		return false;
    	}
    	
		return true;
	}
	
	/**
	 * Get String expression of nodelist if it's not null
	 * @param expression	XPath expression
	 * @return	String response
	 */
	private String getString(String expression, String expectedType) {
		resultNodeList = getNodeList(expression);
		if(resultNodeList.getLength() != 0)
			return resultNodeList.item(0).getTextContent();
		if(expectedType.equals("s"))
			return "null";
		else
			return "0";
	}
	
	/**
	 * Evaluate a given path expression for the object's xpath (reducing code amount)
	 * @param expression Path expression
	 * @return NodeList instance containing desired content
	 * 
	 */
	private NodeList getNodeList(String expression) {
		  try {
			return (NodeList) xpath.evaluate(expression, directionsResponseDocument, XPathConstants.NODESET);
		} catch (XPathExpressionException e) {
			e.printStackTrace();
			this.error += "\nError:\t" + e.getMessage().toString();
			return null;
		}
	  }
	
	/**
	 * Read location and return it
	 * @param pathExpression Path to location information
	 * @return	Location with latitude and longitude
	 * 
	 */
	private Location getLocation(String pathExpression) {
		Location location = new Location("");
		location.setLatitude(Float.NaN);
		location.setLongitude(Float.NaN);
		resultNodeList = getNodeList(pathExpression + "/*");	  
		  for(int i=0; i<resultNodeList.getLength(); ++i) {
		    Node node = resultNodeList.item(i);
		    if("lat".equals(node.getNodeName())) location.setLatitude(Float.parseFloat(node.getTextContent()));
		    if("lng".equals(node.getNodeName())) location.setLongitude(Float.parseFloat(node.getTextContent()));
		  }
		return location;
	}
	
	
	/**
	 * Establish connection and get response into an xml document
	 * @return false if an exception occured
	 */
	private boolean requestRouteInfo() {
		try {
			
			String urlString = DIRECTIONS_REQUEST_PREFIX_FOR_XML + 
			  			"?origin=" 			+ URLEncoder.encode(origin, "UTF-8") 		+ 
			  			"&destination="		+ URLEncoder.encode(destination, "UTF-8") 	+ 
			  			"&sensor=" 			+ URLEncoder.encode(sensor, "UTF-8") 		 +
			  			"&language="			+ URLEncoder.encode(language, "UTF-8") 		 ; 
			
			if(departure_time)
				urlString +=  "&departure_time=" 	+ URLEncoder.encode(timeInSec+"", "UTF-8");
			else
				urlString +=  "&arrival_time=" 	+ URLEncoder.encode(timeInSec+"", "UTF-8");
			
			urlString +=  "&mode="			+ URLEncoder.encode(mode, "UTF-8");
			
			url = new URL(urlString);
			Log.d("Navigation URL", urlString);
			conn = (HttpURLConnection) url.openConnection();
			
			//	open the connection and get results as InputSource.
			conn.connect();
			InputSource geocoderResultInputSource = new InputSource(conn.getInputStream());
			  
			// read result and parse into XML Document
			directionsResponseDocument = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(geocoderResultInputSource);
			
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
			error = e.toString();
			return false;
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
			this.error += "\nError:\t" + e.getMessage().toString();
		} catch (MalformedURLException e) {
			e.printStackTrace();
			this.error += "\nError:\t" + e.getMessage().toString();
		} catch (IOException e) {
			e.printStackTrace();
			this.error += "\nError:\t" + e.getMessage().toString();
		} catch (SAXException e) {
			e.printStackTrace();
			this.error += "\nError:\t" + e.getMessage().toString();
		} finally {
			conn.disconnect();
		}
		return true;
	}
	
	/**
	 * Get String of thrown exception
	 * @return String or null
	 */
	public String getError(){
		return this.error;
	}
	
	/**
	 * Get String of the url
	 * @return url of server request
	 */
	public String getURL(){
		return this.url.toString();
	}

/*	public String getAllInformation(){
		String result = "";
		//result += this.getURL();
		result += "\nResponse Status: " + this.responseStatus;
		
		result += "\nRoute Information: \n" + printRouteInfo();
		
		for(Step s : this.steps){
			result += "\n \n"+s.getTravelMode();
			if(s.getTravelMode().equals("WALKING"))
				result += "\n" + printWalkInfo(s.getGeneralDetails());
			else
				result += "\n" + printTransitInfo(s.getTransitDetails());
			
			for(Step sub : s.getSubSteps()){
				result += "\n \n\t"+sub.getTravelMode();
				if(s.getTravelMode().equals("WALKING"))
					result += "\n\t" + printWalkInfo(sub.getGeneralDetails());
				else
					result += "\n\t" + printTransitInfo(sub.getTransitDetails());
			}
		}
		
		final String err = getError();
		if(!err.equals("")){
			result += "\n\n" + err;
		}
		
		result += "\n\n\n" + getLegalInformation();
		
		return result;
	}*/
	
	/**
	 * Printing general Route information
	 * @return route info
	 */
/*	public String printRouteInfo(){
		String result = "";
		
		result += "Departure: " + this.dep_time_S;
		result += "\nFrom: " + this.start_address;
		result += "\nArrival: " + this.arr_time_S;
		result += "\nTo: " + this.end_address;
		
		result += "\nDistance: " + this.distance_route_S;
		result += "\nDuration: " + this.duration_route_S;
		
		return result;
	}*/
	
	/**
	 * Printing all of the general information
	 * @param walk
	 */
/*	public String printWalkInfo(Walk walk){
		String result = "";
		result += "Instructions: \n\t" + walk.getInstruction();
		result += "\n" + "Duration Text: " + walk.getDurationText();
		result += "\n" + "Distance Text: " + walk.getDistanceText();

		return result;
	  }*/
	
	/**
	 * Printing all of the transit information including general information
	 * @param transit
	 */
//	public String  printTransitInfo(Transit transit){
//		String result = "";
//		result += printWalkInfo(transit.getWalkInfo());
//		result += "\n" + "Line " + transit.getLine().getLineType() + ": " + transit.getLine().getLineName();
//		result += "\n" + "Headsign: " + transit.getHeadsign();
//		result += "\n" + "Departure Stop: " + transit.getDepName() ;
//		result += "\n" + "Departure Time: " + transit.getDepTimeText() ;
//		result += "\n" + "Arrival Stop: " + transit.getArrName() ;
//		result += "\n" + "Arrival Time: " + transit.getArrTimeText();
//		
//		return result;
//	  }

	
	/**
	 * 
	 * How the 2D-List looks like:
	 * ******************************************************************************************************
	 * * icon-URL 1  | icon-URL 2     | ... | last icon-URL                                                 *
	 * * line-name 1 | line-name 2    | ... | last line-name                                                *
	 * * Duration    | Departure-Time | Arrival-Time                                                        *
	 * * step1       | 1st sub-step | 2nd sub-step | ...       | last sub-step                              * 
	 * * step2       | 1st sub-step | 2nd sub-step | ...       | last sub-step                              *
	 * * ...                                                                                                *
	 * * ...                                                                                                *
	 * * last step | 1st sub-step | 2nd sub-step | ...       | last sub-step                                *
	 * * destination                                                                                        *
	 * * legal information                                                                                  *
	 * ******************************************************************************************************
	 * 
	 * 
	 * @return A 2D-List of Strings showing the needed navigation information
	 */
	public ArrayList<ArrayList<String>> getAllSteps(){
		
		//final List with infos
		ArrayList<ArrayList<String>> result = new ArrayList<ArrayList<String>>();
		
		// No results -> return null
		if( !this.responseStatus.equals("OK") ){
			return null;
		}
		
		result.add(new ArrayList<String>());
		result.add(new ArrayList<String>());
		
		// first row in ArrayList is for the Logo URLs and the second for the corresponding line names
		for(int i = 0; i < this.steps.size(); i++){
			if( !steps.get(i).getTravelMode().equals("WALKING") ){
				final Line line = this.steps.get(i).getTransitDetails().getLine();
				result.get(result.size()-2).add(validateString(line.getIconURL()));
				icons.add(getDrawableFromURL("http:"+line.getIconURL()));
				if(line.getVehicleType().equals("BUS"))
					result.get(result.size()-1).add("Bus" + validateString(line.getLineType()));
				else
					result.get(result.size()-1).add(validateString(line.getLineType()));
			}
			else{
				result.get(result.size()-2).add("http://maps.gstatic.com/mapfiles/transit/iw/6/walk.png");
				icons.add(getDrawableFromURL("http://maps.gstatic.com/mapfiles/transit/iw/6/walk.png"));
				result.get(result.size()-1).add(ServerCommunication.map.getString(R.string.walk));
			}
		}
		
		//adding duration and departure-/arrival-time		
		result.add(new ArrayList<String>());
		result.get(result.size()-1).add(validateString(this.duration_route_S));
		result.get(result.size()-1).add(validateString(this.dep_time_S));
		result.get(result.size()-1).add(validateString(this.arr_time_S));
				
		String tmpString = "";
		
		for(Step s : this.steps){
			final Walk walk = s.getGeneralDetails();
			final Transit transit = s.getTransitDetails();
			
			result.add(new ArrayList<String>());
			
			//adding a walking step
			if(s.getTravelMode().equals("WALKING")){
				tmpString += validateString(walk.getInstruction()) + ".\n";
				tmpString += "ca. " + validateString(walk.getDurationText() + " \n");
				tmpString += "(" + validateString(walk.getDistanceText() + ")");
				result.get(result.size()-1).add(tmpString);
			}
			else{ //adding a transit step
				tmpString += validateString(transit.getDepName()) + " - ";
				tmpString += validateString(transit.getArrName()) + "\n";
				tmpString += validateString(transit.getWalkInfo().getInstruction()) + "\n";
				tmpString += validateString(transit.getDepTimeText())+" - ";
				tmpString += validateString(transit.getArrTimeText());
				tmpString += "(" + validateString(transit.getWalkInfo().getDurationText()) + ")";
				result.get(result.size()-1).add(tmpString);

				tmpString = "";
				// get additional info for child node
				tmpString += ServerCommunication.map.getString(R.string.stops) + ": " + validateString(""+transit.getNum_stops())  + "\n";
				tmpString += ServerCommunication.map.getString(R.string.agency)+ ": " + validateString(transit.getLine().getAgency_name());
				result.get(result.size()-1).add(tmpString);
			}
			tmpString = "";
			
			for(int i = 0; i < s.getNumSubStep(); i++){
				final ArrayList<Step> sub = s.getSubSteps();
				tmpString += i + 1 + ". " +  validateString(sub.get(i).getGeneralDetails().getInstruction()) + "\n";
				tmpString += validateString(sub.get(i).getGeneralDetails().getDistanceText());
				
				result.get(result.size()-1).add(tmpString);
				tmpString = "";
			}
		}
		
		result.add(new ArrayList<String>());
		result.get(result.size()-1).add(validateString(start_address) + "\n - \n" + validateString(end_address));

		
		result.add(new ArrayList<String>());
		
		final String err = getError();
		if(!err.equals("")){
			tmpString = err + "\n";
		}
		else{
			tmpString = getLegalInformation();
		}
		
		result.get(result.size()-1).add(tmpString);
		
		return result;
	}
	
	/**
	 * Determine whether a String is null or not. If yes, return an empty String instead of null
	 * @param s String to validate
	 * @return Given string or empty string
	 */
	private String validateString(String s){
		if(!s.equals(null))
			return s;
		else
			return "";
	}
	
	/**
	 * Get a Bitmap object from a Image URL
	 * @param url Adress to the image
	 * @return Bitmap
	 */
	public static Bitmap getBitmapFromURL(String url) {
		try {
			return BitmapFactory.decodeStream((InputStream) new URL(url).getContent());
		} catch (MalformedURLException e) {
			e.printStackTrace();
			return null;
		} catch (IOException e) {
			e.printStackTrace();
			return null;
		}
	}
	
	/**
	 * Get a Drawable object from a Image URL
	 * @param url Adress to the image
	 * @return Drawable
	 */
	public static Drawable getDrawableFromURL(String url) {
		try {
			return Drawable.createFromStream((InputStream) new URL(url).getContent(), "src");
		} catch (MalformedURLException e) {
			e.printStackTrace();
			return null;
		} catch (IOException e) {
			e.printStackTrace();
			return null;
		}
	}
	
	/**
	 * Get the list of Steps for navigation
	 * @return ArrayList
	 */
	public ArrayList<Step> getSteps(){
		return this.steps;
	}
	
	/**
	 * Get the status of the server response
	 * @return
	 */
	public String getResponseStatus(){
		return this.responseStatus;
	}
	
	/**
	 * Get all icons of the navigation
	 * @return
	 */
	public ArrayList<Drawable> getAllIcons(){
		return icons;
	}

	
/*	public int getDuration_route_value() {
		return duration_route_i;
	}


	public String getDuration_route_String() {
		return duration_route_S;
	}


	public int getDistance_route_value() {
		return distance_route_i;
	}


	public String getDistance_route_String() {
		return distance_route_S;
	}


	public Location getStart_route_Location() {
		return start_route;
	}


	public Location getEnd_route_Location() {
		return end_route;
	}


	public String getStart_address() {
		return start_address;
	}


	public String getEnd_address() {
		return end_address;
	}


	public long getDep_time_value() {
		return dep_time_s;
	}


	public String getDep_time_String() {
		return dep_time_S;
	}


	public String getDep_timeZone() {
		return dep_timeZone;
	}


	public long getArr_time_value() {
		return arr_time_s;
	}

	public String getArr_time_String() {
		return arr_time_S;
	}

	public String getArr_timeZone() {
		return arr_timeZone;
	}*/
	
}
